﻿#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
//1. 为什么要有动态内存分配  ---------11
//2. malloc 和 free ------------------23
//3. calloc 和 realloc ---------------68
//4. 常见的动态内存的错误 ------------142
//5. 动态内存经典笔试题分析 ----------214
//6. 柔性数组 ------------------------297
//7. 总结C / C++中程序内存区域划分 ---

//为什么要有动态内存分配
//在前面的学习，我们已经掌握的内存开辟方式有：
    //int val = 20; //在栈空间上开辟4个字节
    //char arr[10] = { 0 }; //在栈空间上开辟10个字节的连续空间 
    //但是上述的开辟空间的方式有两个特点：
        //1.空间开辟大小是固定的
        //2.数组在申明的时候，必须指定数组的长度，数组空间一旦确定了大小不能调整

    //但是对于空间的需求，不仅仅是上述的情况
    //有时候我们需要的空间大小在程序运行的时候才能知道，那数组的编译时开辟空间的方式就不能满足了
    //C语言引入了动态内存开辟，让程序员自己可以申请和释放空间，就比较灵活

//malloc 和 free
//malloc
    //C语言提供了一个动态内存开辟的函数malloc：
    //void* malloc (size_t size); size的单位是字节
    //这个函数向内存申请一块连续可用的空间，并返回指向这块空间的指针。

    //如果开辟成功，则返回一个指向开辟好空间的指针
    //如果开辟失败，比如开辟空间过大找不到合适空间，则返回⼀个 NULL 指针，因此malloc的返回值⼀定要做检查
    //返回值的类型是 void* ，所以 malloc 函数并不知道开辟空间的类型，具体在使用的时候使用者自己来决定

//free
    //C语言提供了另外一个函数 free ，专门是用来做“动态内存”的释放和回收的，函数原型如下：
    //void free (void* ptr); ptr 传的是需要释放空间的起始地址

    //如果参数 ptr 指向的空间不是动态开辟的，例如char arr[10] = { 0 }，那free函数的行为是未定义的
    //如果参数 ptr 是 NULL 指针，则函数什么事都不做

    // malloc 和 free 都声明在 stdlib.h 头文件中

#include <assert.h>
#include <stdlib.h>
//int main()
//{
//    //开辟20个字节 -- 存放5个整型数据
//    int* p = (int*)malloc(20);
//    assert(p != NULL); //判断 p 是否是空指针，防止出问题
//
//    //使用空间
//    int i = 0;
//    for (i = 0; i < 5; i++)
//    {
//        *(p + i) = i + 1;
//    }
//
//    //释放内存
//    free(p);  //传递给 free 函数的是要释放的内存空间的起始地址
//              //所以在上面使用空间时，不能采用 *p = i + 1; p++ 这种方式，
//              //原因就是 p++ 这种形式会使p的起始地址也发生改变，而指针加减整数是不会改变指针本身的值(即起始地址)
//
//    p = NULL; //free 函数的本质是把空间的使用权限还给操作系统
//              //虽然 p 那块空间被释放了，那块空间已经不属于 p 了，但 p 仍然还是指向对应的那个起始地址
//              //此时 p 就是一个野指针，所以需要用NULL空指针赋给 p ，用来提醒
//    return 0;
//}

//calloc 和 realloc
//calloc
    //C语言还提供了一个函数叫 calloc ， calloc 函数也用来动态内存分配。原型如下：
    //void* calloc (size_t num, size_t size);

    //函数的功能是为 num 个大小为 size 的元素开辟一块空间，并且把空间的每个字节初始化为0。
    //与函数 malloc 的区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全0，而 malloc 函数是没有初始化的

//int main()
//{
//    int* p = (int*)calloc(5, sizeof(int));
//    if (NULL != p)
//    {
//        *p = 1;
//        int i = 0;
//        for (i = 0; i < 5; i++)
//        {
//            printf("%d ", *(p + i)); //1 0 0 0 0，后面的4个0就是 calloc 函数默认初始化的
//        }
//    }
//    free(p); 
//    p = NULL;
//    return 0;
//}

//realloc
    // realloc 函数的出现让动态内存管理更加灵活。
    //有时会我们发现过去申请的空间太小了，有时候我们又会觉得申请的空间过大了，
    //那为了合理的使用内存，我们一定会对内存的大小做灵活的调整。那 realloc 函数就可以做到对动态开辟内存大小的调整。

    //函数原型如下：
    //void* realloc (void* ptr, size_t size);
    // ptr 是要调整的内存地址
    // size 调整之后新大小，单位是字节
    //返回值为调整之后的内存起始位置

    //realloc在调整内存空间的是存在两种情况：
    //情况1：原有空间之后有足够大的空间，至少能满足扩展所需空间的大小，则直接追加空间，原来空间的数据不发生变化
    //情况2：原有空间之后没有足够大的空间
        //扩展的方法是：在堆区上另外找一个合适大小的连续空间来使用，将原来空间的数据拷贝一份到该新空间
        //              这样函数返回的是一个新的内存地址的起始地址，并且 realloc 函数会把原来旧的空间释放掉
    //还有一种特殊的情况就是如果调整失败，比如所需大小过大导致堆区找不到这样的空间，则返回的是 NULL

//int main()
//{
//    int* p = malloc(20);
//    assert(p != NULL);
//    for (int i = 0; i < 5; i++)
//    {
//        *(p + i) = i + 1;
//    } //1 2 3 4 5
//    
//    //希望将空间调整为40个字节大小
//    int* ptr = (int*)realloc(p, 40); //在调用 realloc 函数时，不能赋给原来的指针，而是先赋给一个新指针
//                                     //原因是如果是特殊情况，即调整失败返回的是 NULL 空指针，
//                                     //此时如果赋给原来的指针，不仅没有得到调整成功的空间，原来调整前的空间也没了，肯定不行
//    assert(ptr != NULL); //检验调整是否成功，如果成功再赋给原来的指针
//    p = ptr;
//    for (int i = 5; i < 10; i++)
//    {
//        *(p + i) = i + 1; //由于原来的数据已经拷贝一份到新空间了所以不需要再重新开始赋值
//    }
//    free(p);
//    p = NULL;
//    return 0;
//}

    // raalloc 函数也能完成和 malloc 函数一样的功能：
//int main()
//{
//    realloc(NULL, 20); // ==malloc(20);
//    return 0;
//}

//常见的动态内存的错误
//1. 对 NULL 指针的解引用操作
//void test()
//{
//    int* p = (int*)malloc(INT_MAX / 4); //如果找不到满足括号内大小的空间，则返回 NULL 指针
//    *p = 20; //如果p的值是 NULL ，就会有问题
//    free(p);
//}

//2. 对动态开辟空间的越界访问
//void test()
//{
//    int i = 0;
//    int* p = (int*)malloc(10 * sizeof(int));
//    if (p == NULL)
//    {
//        exit(EXIT_FAILURE);
//    }
//    for (i = 0; i <= 10; i++)
//    {
//        *(p + i) = i;  //当i是10的时候越界访问
//    }
//    free(p);
//}

//3. 对非动态开辟内存使用free释放
//void test() 
//{
//    int a = 10;
//    int* p = &a; //a是局部变量，不是在堆区生成而是在栈区上生成的，并不是malloc或calloc等生成的动态内存
//    free(p);     //free只能释放动态内存
//    p = NULL;
//}

//4. 使用free释放一块动态开辟内存的一部分
//void test()
//{
//    int* p = (int*)malloc(100);
//    assert(p != NULL);
//    for (int i = 0; i < 5; i++)
//    {
//        *p = i + 1;
//        p++;
//    }
//    free(p);     // p++使得最终 p 不再指向动态内存的起始位置
//    p = NULL;    //而free释放的动态内存必须是起始位置
//}

//5. 对同一块动态内存多次释放
//void test()
//{
//    int* p = (int*)malloc(100);
//    free(p);                     //虽然连续多次释放是错误的，但如果在释放完内存后把 NULL 空指针赋给p
//    free(p);//重复释放           //再释放时相当于就是free(NULL)，此时 free 函数什么都不会做
//}

//6. 动态开辟内存忘记释放(内存泄漏)
//void test()
//{
//    int* p = (int*)malloc(100);
//    if (NULL != p)
//    {
//        *p = 20;
//    }             //在自定义函数中开辟了动态内存p，使用完后却没有及时free函数回收内存，
//}                 //而当回到主函数时在自定义函数创建的局部变量就会被自动销毁，
//                  //此时已经找不到了，释放也就释放不了了，从而导致内存泄漏
//int main()
//{
//    test();
//    while (1);
//}

//动态内存经典笔试题分析
//题目1：
//void GetMemory(char* p)
//{
//    p = (char*)malloc(100);
//}
//void Test(void)
//{
//    char* str = NULL;
//    GetMemory(str);
//    strcpy(str, "hello world");
//    printf(str);
//}
//int main()
//{
//    Test();
//    return 0;
//}
    //出现的问题：1.内存泄露，调用完 malloc 函数之后并没有用 free 函数将内存释放
    //            2.程序崩溃
    //首先是将空指针赋给str，然后将str的值传给形参p，形参p只是str的临时拷贝，p单独开辟一个空间，只是放的内容是str的值
    //调用 malloc 函数赋给形参p，让其开辟100个字节空间，而当出了函数后，操作系统又会自动将p销毁，空间还给了操作系统
    //所以回到 Test 函数时，str仍然为空指针，原因就是p只是str是临时拷贝，对p进行修改不会改变str
    //然后再用 strcpy 函数将"hello world"拷贝给str
    //由于之前学strcpy我们就知道在调用像这样的字符串函数之前必须检查被拷贝的对象是否为空指针，如果是空指针是不能进行拷贝的
    //原因就在于调用这类函数必然会存在对指针解引用的操作，而对空指针解引用就会导致程序崩溃

    //改进措施：
//void GetMemory(char** p)
//{
//    *p = (char*)malloc(100);
//}
//void Test(void)
//{
//    char* str = NULL;
//    GetMemory(&str);             //这个改进方法和之前学习自定义函数时传参一样，
//    strcpy(str, "hello world");  //之前是传递值不行，进而改成传递其地址，原因是传递地址后
//    printf(str);                 //形参解引用进行修改后，其地址仍然是实参地址，出了函数后虽然形参被操作系统销毁，
//    free(str);                   //但不像传值，传值后修改形参是不会影响实参内容，因为两者地址就不一样
//    str = NULL;                  //而传地址两者的地址就是一样的，修改形参内容，实参内容也会被修改
//}
//int main()
//{
//    Test();
//    return 0;
//}

//题目2：
//char* GetMemory(void)
//{
//    char p[] = "hello world";
//    return p;                     //这就是典型的返回栈空间地址的问题
//}                                 //本质还是在函数内部进行空间开辟和赋值出了函数空间就会被销毁
//void Test(void)                   // GetMemory 函数中创建字符数组p内容，为"hello world"
//{                                 //当出了函数返回后，用str指针访问p数组时就是非法访问
//    char* str = NULL;             //就是因为出了 GetMemory 函数后p数组的内存已经被操作系统回收了
//    str = GetMemory();            //此时再赋给str则就是野指针了，打印的内容就是乱码
//    printf(str);
//}
//int main()
//{
//    Test();
//    return 0;
//}

//题目3：
//void GetMemory(char** p, int num)
//{
//    *p = (char*)malloc(num);       //这个题目其实就和题目1修改措施基本一样，
//}                                  //运行是不会出现报错和崩溃的
//void Test(void)                    //唯一的一处问题其实就是调用 malloc 函数后没有用 free 函数将其内存释放
//{
//    char* str = NULL;
//    GetMemory(&str, 100);
//    strcpy(str, "hello");
//    printf(str);
//}
//int main()
//{
//    Test();
//    return 0;
//}

//柔性数组
//柔性数组的概念：
    //结构中的最后一个元素允许是未知大小的数组，这就叫做“柔性数组”成员。
        //1.是在结构体中，并且是最后一个成员
        //2.最后一个成员是数组，并且没有指定数组的大小

//struct S
//{
//    char c;
//    int n;
//    int arr[]; //柔性数组
//};

//柔性数组的特点：
    //1.结构中的柔性数组成员前面必须至少一个其他成员
    //2. sizeof 返回的这种结构大小不包括柔性数组的内存。

//struct S
//{
//    char c;
//    int n;
//    int arr[]; 
//};
//int main()
//{
//    printf("%zd\n", sizeof(struct S)); //8
//    return 0;
//}

  //3.包含柔性数组成员的结构用 malloc 函数进行内存的动态分配，并且分配的内存应该大于结构的大小，以适应柔性数组的预期大小。
//struct S
//{
//    int n;
//    int arr[]; 
//};
//int main()
//{
//    struct S* ps = (struct S*)malloc(sizeof(struct S) + 5 * sizeof(int));
//    //                              上面我们知道 sizeof 返回的结构大小不包括柔性数组的内存，所以结构体S大小只有int n的
//    //                              为了希望柔性数组也能放进结构体里，需要用 malloc 函数开辟空间，
//    //                              并且额外加上柔性数组所需的内容大小的字节数，在这里我们就需要柔性数组arr存放5个整型数据
//    assert(ps != NULL);
//
//    //对结构体输入值：
//    ps->n = 100;
//    for (int i = 0; i < 5; i++)
//    {
//        ps->arr[i] = i;
//    }
//
//    //用 realloc 函数调整结构体大小(在这里其实就说明为什么是叫做柔性数组，调整结构体大小变相的就是调整柔性数组的大小)
//    struct S* pss = (struct S*)realloc(ps, sizeof(struct S) + 10 * sizeof(int));
//    assert(pss != NULL);
//    ps = pss;
//    for (int i = 5; i < 10; i++)
//    {
//        ps->arr[i] = i;
//    }
//
//    //释放动态内存
//    free(ps);
//    ps = NULL;
//    return 0;
//}
